// Copyright (C) ZHENG Zhong <heavyzheng nospam-at gmail D0T com>
//
// $Id: function.cpp 227 2008-09-15 13:33:40Z heavyzheng $
//

#ifndef ZZHENG_QUANT_DATES_SOURCE
#define ZZHENG_QUANT_DATES_SOURCE
#endif // ZZHENG_QUANT_DATES_SOURCE

#include <zzheng/quant/dates/function.hpp>
#include <zzheng/quant/dates/exception.hpp>
#include <zzheng/quant/dates/date.hpp>
#include <zzheng/quant/dates/duration.hpp>
#include <zzheng/quant/dates/calendar.hpp>

#ifdef ZZHENG_MSVC
#  pragma warning(push)
#  pragma warning(disable: 4127 4511 4512 4701)
#endif // ZZHENG_MSVC

#include <boost/date_time/gregorian/gregorian.hpp>
#include <boost/algorithm/string/trim.hpp>

#ifdef ZZHENG_MSVC
#  pragma warning(pop)
#endif // ZZHENG_MSVC

#include <cassert>
#include <exception>
#include <string>
#include <sstream>

namespace zzheng {
namespace quant {
namespace dates {

    date parse_date(const std::string& str) {
        try {
            return date(boost::gregorian::from_string(str));
        } catch (const std::exception& ex) {
            std::string errmsg = "Fail to parse string '" + str + "' to date: " + ex.what();
            throw date_error(errmsg.c_str());
        }
    }

    date today() {
        return date(boost::gregorian::day_clock::local_day());
    }

    date create_from_excel_day(int excel_day) {
        date d;
        if (excel_day >= 0) {
            d = date(1899, 12, 30); // Excel day 0.
            d.add_days(excel_day);
        }
        return d;
    }

    int get_excel_day(const date& d) {
        const long JULIAN_DAY_1899_12_30 = 2415019L;
        return (d.julian_day() - JULIAN_DAY_1899_12_30);
    }

    date adjust(const date& d, rolling_t rolling, const calendar& cal) {
        if (rolling == no_adjustment) {
            return d;
        } else {
            date adjusted = d;
            // Adjust the date to preceding or following, regardless of modified.
            while (!cal.is_business_day(adjusted)) {
                switch (rolling) {
                case preceding:
                case mod_preceding:
                    --adjusted;
                    break;
                case following:
                case mod_following:
                    ++adjusted;
                    break;
                default:
                    assert(! "Invalid rolling convention.");
                    ++adjusted; // use following.
                }
            }
            // If the adjusted date falls into another month and rolling is modified, re-adjust.
            if (adjusted.month() != d.month()) {
                switch (rolling) {
                case mod_preceding:
                    adjusted = adjust(d, following, cal);
                    break;
                case mod_following:
                    adjusted = adjust(d, preceding, cal);
                    break;
                default:
                    // Rolling is not modified: no more adjustment.
                    break;
                }
            }
            // Return the adjusted date.
            return adjusted;
        }
    }

    date operator+(const date& d, int delta) {
        date result = d;
        result.add_days(delta);
        return result;
    }

    date operator-(const date& d, int delta) {
        date result = d;
        result.add_days(-delta);
        return result;
    }

    duration parse_duration(const std::string& str) {
        // Strip off preceding and following white spaces, and check bad characters.
        std::string str2 = boost::trim_copy(str);
        if (str2.find_first_of(" \n\r\t\b") != std::string::npos) {
            std::string errmsg = "Fail to parse string '" + str + "' to duration: "
                               + "bad chararcter(s) found.";
            throw date_error(errmsg.c_str());
        }
        // Parse string to duration count and unit.
        std::istringstream iss(str2);
        duration::count_type count = 0;
        std::string unit_str;
        iss >> count >> unit_str;
        // Check for validity and construct duration.
        if (iss.fail()) {
            std::string errmsg = "Fail to parse string '" + str + "' to duration: iostate is fail.";
            throw date_error(errmsg.c_str());
        } else if (iss.bad()) {
            std::string errmsg = "Fail to parse string '" + str + "' to duration: iostate is bad.";
            throw date_error(errmsg.c_str());
        } else {
            duration::unit_type unit = duration::unknown;
            if (unit_str.size() == 1U) {
                switch (unit_str.at(0)) {
                case 'd':
                case 'D':
                    unit = duration::day;
                    break;
                case 'w':
                case 'W':
                    unit = duration::week;
                    break;
                case 'm':
                case 'M':
                    unit = duration::month;
                    break;
                case 'y':
                case 'Y':
                    unit = duration::year;
                    break;
                default:
                    break;
                }
            }
            if (unit == duration::unknown) {
                std::string errmsg = "Fail to parse string '" + str + "' to duration: "
                                   + "invalid duration unit '" + unit_str + "'.";
                throw date_error(errmsg.c_str());
            } else {
                return duration(count, unit);
            }
        }
    }

    duration make_days(duration::count_type count) {
        return duration(count, duration::day);
    }

    duration make_weeks(duration::count_type count) {
        return duration(count, duration::week);
    }

    duration make_months(duration::count_type count) {
        return duration(count, duration::month);
    }

    duration make_years(duration::count_type count) {
        return duration(count, duration::year);
    }

    date add_duration(const date& d, const duration& delta, bool end_of_month) {
        date result = d;
        switch (delta.unit()) {
        case duration::day:
            result.add_days(delta.count());
            break;
        case duration::week:
            result.add_weeks(delta.count());
            break;
        case duration::month:
            result.add_months(delta.count(), end_of_month);
            break;
        case duration::year:
            result.add_years(delta.count(), end_of_month);
            break;
        default:
            assert(! "Invalid duration unit.");
            break;
        }
        return result;
    }

    calendar get_dummy_calendar() {
        calendar dummy("dummy");
        dummy.clear_holidays();
        dummy.clear_weekends();
        dummy.add_weekend(date::saturday);
        dummy.add_weekend(date::sunday);
        return dummy;
    }

} // namespace zzheng::quant::dates
} // namespace zzheng::quant
} // namespace zzheng




